51. 数据框缺失值处理

⭐ 本章概览:为什么要处理缺失值?

缺失值(Missing Values)是数据分析中无法回避的普遍问题。

  • 金融市场中尤为常见:停牌、新股上市、节假日休市等
  • 简单忽略会导致统计偏倚信息损失
  • 大多数算法(回归、神经网络等)无法直接处理缺失值

本章核心任务:学会识别、诊断和处理数据框中的缺失值。

⭐ 金融数据缺失的典型场景

场景 说明
停牌事件 股票因重大事项停牌,期间无交易数据
新股上市 上市前无历史数据,产生”左截断”问题
节假日效应 A股春节、国庆休市,海外市场继续交易
数据源故障 数据供应商技术问题导致部分数据不可用
公司退市 退市公司被剔除,历史数据不再维护

⭐ 不能简单忽略缺失值的三大原因

  1. 统计偏倚:若缺失非随机,删除会导致样本有偏
    • 例:删除退市公司数据会高估股票真实收益
  2. 信息损失:A股约5000只股票,样本有限,每一笔数据都珍贵
  3. 算法要求:线性回归、神经网络等均无法直接处理缺失值

⭐ Pandas中的缺失值表示:NaN

Pandas使用 NaN(Not a Number)表示数值型缺失值。

NaN的特殊性质

  • NaN != NaN(自反性不成立)
  • NaN + x = NaN(吸收律:任何运算结果仍为NaN)
  • 永远不要用 == 判断NaN,应使用 pd.isna()np.isnan()

⭐ NaN的数学特性演示

Listing 1
import numpy as np
import pandas as pd

nan_value = np.nan

# NaN的比较特性
print('NaN的比较特性:')
print(f'NaN == NaN: {nan_value == nan_value}')   # False!
print(f'NaN != NaN: {nan_value != nan_value}')   # True!
print(f'NaN is NaN: {nan_value is nan_value}')   # True

# NaN的运算特性(吸收律)
print('\nNaN的运算特性:')
print(f'NaN + 100: {nan_value + 100}')   # NaN
print(f'NaN * 2: {nan_value * 2}')       # NaN
NaN的比较特性:
NaN == NaN: False
NaN != NaN: True
NaN is NaN: True

NaN的运算特性:
NaN + 100: nan
NaN * 2: nan

⭐ 缺失值检测方法

Listing 2
import pandas as pd
import numpy as np

# 创建包含缺失值的示例数据
data = {
    '股票代码': ['600519.SH', '000858.SZ', '600036.SH', '601318.SH', '000001.SZ'],
    '收盘价': [1850.0, np.nan, 45.2, 52.8, np.nan],
    '涨跌幅': [0.05, -0.02, np.nan, -0.01, 0.03],
    '成交量': [1200, 3500, np.nan, 5600, 2800]
}
df = pd.DataFrame(data)
print('原始数据:')
print(df)
原始数据:
        股票代码     收盘价   涨跌幅     成交量
0  600519.SH  1850.0  0.05  1200.0
1  000858.SZ     NaN -0.02  3500.0
2  600036.SH    45.2   NaN     NaN
3  601318.SH    52.8 -0.01  5600.0
4  000001.SZ     NaN  0.03  2800.0

⭐ 统计每列的缺失值数量与比例

Listing 3
# 每列缺失值数量
print('每列缺失值数量:')
print(df.isna().sum())

# 每列缺失值比例
print('\n每列缺失值比例(%):')
print((df.isna().sum() / len(df) * 100).round(2))

# 完整行数统计
complete = df.dropna()
print(f'\n完整行数: {len(complete)} / {len(df)}')
每列缺失值数量:
股票代码    0
收盘价     2
涨跌幅     1
成交量     1
dtype: int64

每列缺失值比例(%):
股票代码     0.0
收盘价     40.0
涨跌幅     20.0
成交量     20.0
dtype: float64

完整行数: 2 / 5

⭐ 缺失值模式分析

理解哪些变量倾向于一起缺失,有助于诊断缺失原因。

Listing 4
def missing_correlation(df):
    """计算变量间的缺失模式相关性"""
    return df.isna().corr()

miss_corr = missing_correlation(df)
print('缺失模式相关性:')
print(miss_corr.round(2))
# 相关系数接近1:两变量倾向于同时缺失
缺失模式相关性:
      股票代码   收盘价   涨跌幅   成交量
股票代码   NaN   NaN   NaN   NaN
收盘价    NaN  1.00 -0.41 -0.41
涨跌幅    NaN -0.41  1.00  1.00
成交量    NaN -0.41  1.00  1.00

⭐ 删除策略概览

场景 推荐策略 理由
缺失<5%且随机 dropna() 信息损失小
关键指标缺失 dropna(subset=[...]) 关键指标不可缺
缺失>50% 考虑删除该变量 信息太少,插补不可靠

⭐ dropna的多种删除策略

Listing 5
# 策略1:删除包含任何缺失值的行
df_any = df.dropna(how='any')
print('删除任何缺失值的行:')
print(df_any)

# 策略2:删除全部为缺失值的行
df_all = df.dropna(how='all')
print('\n删除全部为缺失值的行:')
print(df_all)

# 策略3:删除特定列中有缺失值的行
df_subset = df.dropna(subset=['收盘价', '涨跌幅'])
print('\n在收盘价或涨跌幅有缺失的行被删除:')
print(df_subset)
删除任何缺失值的行:
        股票代码     收盘价   涨跌幅     成交量
0  600519.SH  1850.0  0.05  1200.0
3  601318.SH    52.8 -0.01  5600.0

删除全部为缺失值的行:
        股票代码     收盘价   涨跌幅     成交量
0  600519.SH  1850.0  0.05  1200.0
1  000858.SZ     NaN -0.02  3500.0
2  600036.SH    45.2   NaN     NaN
3  601318.SH    52.8 -0.01  5600.0
4  000001.SZ     NaN  0.03  2800.0

在收盘价或涨跌幅有缺失的行被删除:
        股票代码     收盘价   涨跌幅     成交量
0  600519.SH  1850.0  0.05  1200.0
3  601318.SH    52.8 -0.01  5600.0

⭐ 缺失值填充方法:常数填充

Listing 6
# 方法1:用0填充(适合成交量等,不适合价格)
df_zero = df.fillna(0)
print('用0填充:')
print(df_zero)
用0填充:
        股票代码     收盘价   涨跌幅     成交量
0  600519.SH  1850.0  0.05  1200.0
1  000858.SZ     0.0 -0.02  3500.0
2  600036.SH    45.2  0.00     0.0
3  601318.SH    52.8 -0.01  5600.0
4  000001.SZ     0.0  0.03  2800.0

⭐ 均值填充与中位数填充

Listing 7
# 方法2:用均值填充
mean_price = df['收盘价'].mean()
df_mean = df.fillna({'收盘价': mean_price})
print(f'用均值填充(均值={mean_price:.2f}):')
print(df_mean)

# 方法3:用中位数填充(对异常值更稳健)
median_price = df['收盘价'].median()
df_median = df.fillna({'收盘价': median_price})
print(f'\n用中位数填充(中位数={median_price:.2f}):')
print(df_median)
用均值填充(均值=649.33):
        股票代码          收盘价   涨跌幅     成交量
0  600519.SH  1850.000000  0.05  1200.0
1  000858.SZ   649.333333 -0.02  3500.0
2  600036.SH    45.200000   NaN     NaN
3  601318.SH    52.800000 -0.01  5600.0
4  000001.SZ   649.333333  0.03  2800.0

用中位数填充(中位数=52.80):
        股票代码     收盘价   涨跌幅     成交量
0  600519.SH  1850.0  0.05  1200.0
1  000858.SZ    52.8 -0.02  3500.0
2  600036.SH    45.2   NaN     NaN
3  601318.SH    52.8 -0.01  5600.0
4  000001.SZ    52.8  0.03  2800.0

⭐ 平台任务1:检测缺失值

Listing 8
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
import pandas as pd  # 导入Pandas数据分析库
index_bric = pd.read_excel("https://huoran.oss-cn-shenzhen.aliyuncs.com/20220820/xlsx/1560916966116974592.xlsx",sheet_name="Sheet1",header=0,index_col=0) #导入数据
print(index_bric.isnull().any())    #查找每一列是否存在缺失值(用isnull函数)

print(index_bric.isna().any())     #查找每一列是否存在缺失值(用isna函数)
print(index_bric[index_bric.isnull().values==True]) #查找存在缺失值所在的行
IBOVESPA指数    False
RTS指数          True
Sensex30指数     True
上证综指           True
dtype: bool
IBOVESPA指数    False
RTS指数          True
Sensex30指数     True
上证综指           True
dtype: bool
            IBOVESPA指数    RTS指数  Sensex30指数       上证综指
交易日                                                   
2019-06-05    95998.75  1303.35         NaN  2861.4181
2019-06-07    97821.26  1325.95  39615.8984        NaN
2019-06-12    98320.88      NaN  39756.8086  2909.3796

⭐ 平台任务2:删除缺失值行

Listing 9
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
import pandas as pd  # 导入Pandas数据分析库
index_bric = pd.read_excel("https://huoran.oss-cn-shenzhen.aliyuncs.com/20220820/xlsx/1560916966116974592.xlsx",sheet_name="Sheet1",header=0,index_col=0) #导入数据

index_bric_dropna = index_bric.dropna()  #删除存在缺失值的行数并创建一个新的数据框
print(index_bric_dropna.isnull().any())  # 输出缺失值检查结果
print(index_bric.shape)          #查看原数据框的形状参数
print(index_bric_dropna.shape)      #查看新数据框的形状参数
IBOVESPA指数    False
RTS指数         False
Sensex30指数    False
上证综指          False
dtype: bool
(20, 4)
(17, 4)

⭐ 平台任务3:前向补齐

Listing 10
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
import pandas as pd  # 导入Pandas数据分析库
index_bric = pd.read_excel("https://huoran.oss-cn-shenzhen.aliyuncs.com/20220820/xlsx/1560916966116974592.xlsx",sheet_name="Sheet1",header=0,index_col=0) #导入数据

index_bric_ffill = index_bric.fillna(method="ffill")   #向前补齐
print(index_bric_ffill.isnull().any())  # 输出缺失值检查结果
print(index_bric_ffill.loc["2019-06-04":"2019-06-12"])  # 输出2019-06-04
IBOVESPA指数    False
RTS指数         False
Sensex30指数    False
上证综指          False
dtype: bool
            IBOVESPA指数    RTS指数  Sensex30指数       上证综指
交易日                                                   
2019-06-04    97380.28  1307.55  40083.5391  2862.2803
2019-06-05    95998.75  1303.35  40083.5391  2861.4181
2019-06-06    97204.85  1319.85  39529.7188  2827.7978
2019-06-07    97821.26  1325.95  39615.8984  2827.7978
2019-06-10    97466.69  1335.71  39784.5195  2852.1302
2019-06-11    98960.00  1343.33  39950.4609  2925.7162
2019-06-12    98320.88  1343.33  39756.8086  2909.3796

⭐ 平台任务4:后向补齐

Listing 11
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
import pandas as pd  # 导入Pandas数据分析库
index_bric = pd.read_excel("https://huoran.oss-cn-shenzhen.aliyuncs.com/20220820/xlsx/1560916966116974592.xlsx",sheet_name="Sheet1",header=0,index_col=0) #导入数据

index_bric_bfill = index_bric.fillna(method="bfill")   #向后补齐
print(index_bric_bfill.isnull().any())  # 输出缺失值检查结果
print(index_bric_bfill.loc["2019-06-04":"2019-06-13"])  # 输出2019-06-04
IBOVESPA指数    False
RTS指数         False
Sensex30指数    False
上证综指          False
dtype: bool
            IBOVESPA指数    RTS指数  Sensex30指数       上证综指
交易日                                                   
2019-06-04    97380.28  1307.55  40083.5391  2862.2803
2019-06-05    95998.75  1303.35  39529.7188  2861.4181
2019-06-06    97204.85  1319.85  39529.7188  2827.7978
2019-06-07    97821.26  1325.95  39615.8984  2852.1302
2019-06-10    97466.69  1335.71  39784.5195  2852.1302
2019-06-11    98960.00  1343.33  39950.4609  2925.7162
2019-06-12    98320.88  1346.98  39756.8086  2909.3796
2019-06-13    98773.70  1346.98  39741.3594  2910.7406

⭐ 插值法填充缺失值

Listing 12
import numpy as np
import pandas as pd

# 创建包含缺失值的时间序列数据
dates = pd.date_range('2024-01-01', periods=10)
ts_data = pd.DataFrame({
    '日期': dates,
    '价格': [10.5, np.nan, np.nan, 11.2, np.nan,
             11.8, np.nan, np.nan, 12.5, 12.8]
})

# 线性插值:在两个已知点之间画直线
ts_linear = ts_data.copy()
ts_linear['价格_线性插值'] = ts_linear['价格'].interpolate(method='linear')
print('线性插值:')
print(ts_linear)
线性插值:
          日期    价格    价格_线性插值
0 2024-01-01  10.5  10.500000
1 2024-01-02   NaN  10.733333
2 2024-01-03   NaN  10.966667
3 2024-01-04  11.2  11.200000
4 2024-01-05   NaN  11.500000
5 2024-01-06  11.8  11.800000
6 2024-01-07   NaN  12.033333
7 2024-01-08   NaN  12.266667
8 2024-01-09  12.5  12.500000
9 2024-01-10  12.8  12.800000

⭐ 金融应用案例:停牌数据处理

Listing 13
import pandas as pd
import numpy as np

# 模拟招商银行停牌场景
dates = pd.date_range('2024-01-01', periods=10)
suspension_data = pd.DataFrame({
    '日期': dates,
    '收盘价': [10.5, 10.8, 11.0, np.nan, np.nan,
               np.nan, np.nan, np.nan, 11.5, 11.7]
})
suspension_data['是否停牌'] = suspension_data['收盘价'].isna()
print('停牌数据:')
print(suspension_data)
停牌数据:
          日期   收盘价   是否停牌
0 2024-01-01  10.5  False
1 2024-01-02  10.8  False
2 2024-01-03  11.0  False
3 2024-01-04   NaN   True
4 2024-01-05   NaN   True
5 2024-01-06   NaN   True
6 2024-01-07   NaN   True
7 2024-01-08   NaN   True
8 2024-01-09  11.5  False
9 2024-01-10  11.7  False

⭐ 停牌数据:两种处理策略

Listing 14
# 策略1:前向填充(金融分析标准方法)
df_strategy1 = suspension_data.copy()
df_strategy1['收盘价'] = df_strategy1['收盘价'].fillna(method='ffill')
print('策略1 - 前向填充:')
print(df_strategy1)

# 策略2:删除停牌行
df_strategy2 = suspension_data.dropna(subset=['收盘价'])
print('\n策略2 - 删除停牌行:')
print(df_strategy2)
策略1 - 前向填充:
          日期   收盘价   是否停牌
0 2024-01-01  10.5  False
1 2024-01-02  10.8  False
2 2024-01-03  11.0  False
3 2024-01-04  11.0   True
4 2024-01-05  11.0   True
5 2024-01-06  11.0   True
6 2024-01-07  11.0   True
7 2024-01-08  11.0   True
8 2024-01-09  11.5  False
9 2024-01-10  11.7  False

策略2 - 删除停牌行:
          日期   收盘价   是否停牌
0 2024-01-01  10.5  False
1 2024-01-02  10.8  False
2 2024-01-03  11.0  False
8 2024-01-09  11.5  False
9 2024-01-10  11.7  False

⭐ 停牌处理策略比较

策略 优点 缺点
前向填充 简单直观,保持时间连续性 低估真实波动率,延迟反映价格跳跃
删除行 避免虚假数据 破坏时间连续性,丢失信息

金融实践建议

  • 计算收益率时:前向填充是标准方法
  • 计算波动率时:建议删除停牌行

⭐ 不同插值方法的比较

Listing 15
import numpy as np
import pandas as pd

# 创建测试数据(带噪声的正弦波)
x = np.linspace(0, 10, 20)
y_true = np.sin(x) + np.random.normal(0, 0.1, 20)

# 人为制造缺失值
y_missing = y_true.copy()
y_missing[np.random.choice(20, 5, replace=False)] = np.nan

# 线性插值 vs 样条插值
y_linear = pd.Series(y_missing).interpolate(method='linear')
y_spline = pd.Series(y_missing).interpolate(method='cubic')

# 计算MSE
print(f'线性插值MSE: {np.mean((y_true - y_linear)**2):.4f}')
print(f'样条插值MSE: {np.mean((y_true - y_spline)**2):.4f}')
线性插值MSE: 0.0075
样条插值MSE: 0.0056

⭐ 本章小结

  • 识别缺失值:使用 isna() / isnull() 检测,sum() 统计
  • 删除策略dropna(how='any') / dropna(subset=[...]) 按需选择
  • 常数填充fillna(0) / fillna(均值) / fillna(中位数)
  • 方向填充fillna(method='ffill') 前向 / fillna(method='bfill') 后向
  • 插值法interpolate(method='linear') 线性 / 'cubic' 样条
  • 金融实践:停牌数据推荐使用前向填充